<?php

/***  DOCUMENTATION LAYER

Ceo Framework Err Class

Name: CeoErr
Last Update: Sep 2007
Author: Tom at klenwell@gmail.com

DESCRIPTION
  Error handler

METHODS
  MAGIC
  CeoErr($debug=0, $oid=null)          *php 4 constructor*
  __construct($debug, $oid)          *php 5 constructor*
  __destruct()  
  
  PUBLIC
  trigger($message, $level)
  set_handler($Callback)
  set_handler_by_release_stage($stage)
  kernel_handler()
  alpha_handler()
  beta_handler()
  final_handler()
  shutdown()
  print_d($message, $color='c33')
  print_r()
  dump()
  
  PRIVATE
  _display()
  _log()
  _email()
  _build_display()
  _get_bugtrace()
  _is_fatal()  
  _is_serious()
  _is_index_notice()
  _is_strict()  
  _read_timelog()
  _write_timelog()
  _set_filename()
  _set_dirpath()
  
USAGE
  $Err = new CeoErr();


NOTES
  _read_timelog and _write_timelog methods are used to limit email notice (no use
  getting 500 emails in 2 mins for the same error)

______________________________________________________________________________*/

// Load File of Base Class (n/a)


// Class
/*____________________________________________________________________________*/
class CeoErr
{
/* PUBLIC PROPERTIES */
var $debug = 0;
var $class_name = __CLASS__;
var $oid = '';
var $DS = DIRECTORY_SEPARATOR;

// public arrays
var $CSS = array();

// public config vars
var $filter_trivial = 1;            // filter trivial (for alpha stage)
var $email_alpha = 0;               // to email errors even in alpha testing
var $max_log_size = 50000;          // max error log size (will be overwritten exceeded)
var $kill_logging = 0;              // force logging off
var $script_email_limit = 1;        // email max per script
var $session_email_limit = 2;       // email max per session
var $session_email_delay = 600;     // delay before sending the next email between session

// vars
var $log_path = '';
var $handler_name = '';
var $count = 0;
var $strict_count = 0;

// error data
var $num = 0;
var $msg = '';
var $file = '';
var $line = '';
var $uts = 0;
var $date = '';
var $server = '';
var $type = '';
var $basename = '';
var $trigger_file = '';
var $trigger_line = '';
var $is_index_notice = 0;
var $is_strict = 0;
var $is_notice = 0;
var $is_trivial = 0;
var $is_serious = 0;
var $is_fatal = 0;
var $bugtrace = '';


/* PRIVATE PROPERTIES */

// Private Arrays
var $_TYPE = array();
var $_MASK = array();
var $_ERR_FUNCTION = array();
var $_STRICT_LOG = array();

var $_sid = 'error_session_id';     // session id (reset in cx)
var $_timelog_path = '';
var $_script_emails_sent = 0;
var $_Mailer = 0;

var $_filename = '';
var $_dirpath = '';


/* ** MAGIC METHODS ** */
// php4 constructor
function CeoErr($debug=0, $oid=null)
{
  $this->__construct($debug, $oid);
  register_shutdown_function( array($this, '__destruct') );
}
// END constructor

// php5 constructor
function __construct($debug=0, $oid=null)
{
  // default
  $this->debug = $debug;
  $this->oid = ( empty($oid) ) ? $this->class_name : $oid;
  $this->_set_filename();
  $this->_set_dirpath();
  
  // class session id
  $this->_sid = $this->oid . '_sid';
  
  // error handler functions (these are skipped in backtraces)
  $this->_ERR_FUNCTION = array('trigger', '_collect_error_data');
  
  // log paths
  $this->log_path = $this->_dirpath . 'log_bin' . $this->DS . 'error.log';
  $this->_timelog_path = $this->_dirpath . 'log_bin' . $this->DS . 'error.last_email.txt';
  
  // magic values
  $this->_MASK['serious'] = 1015;     // 001111110111
  $this->_MASK['fatal'] = 341;        // 000101010101
  
  // error name
  $this->_TYPE = array
  (
    E_ERROR              => 'Error',
    E_WARNING            => 'Warning',
    E_PARSE              => 'Parsing Error',
    E_NOTICE             => 'Notice',
    E_CORE_ERROR         => 'Core Error',
    E_CORE_WARNING       => 'Core Warning',
    E_COMPILE_ERROR      => 'Compile Error',
    E_COMPILE_WARNING    => 'Compile Warning',
    E_USER_ERROR         => 'User Error',
    E_USER_WARNING       => 'User Warning',
    E_USER_NOTICE        => 'User Notice',
    E_STRICT             => 'Runtime Notice',
  );
    
  // set styles
  $this->CSS['bugtrace'] = 'padding:2px 4px; line-height:1.6em; color:#ffcccc; background:#660000;';
  $this->CSS['display_count'] = 'position:absolute; top:-1px; right:2px; font-size:8px;';
  $this->CSS['display_header'] = 'font-size:110%;';
  $this->CSS['display_footer'] = 'color:#ff0000; font-weight:bold; font-size:100%;';
  $this->CSS['display_trace'] = 'margin:2px 0; padding:2px 4px; color:#fee; background:#660000;';
  $this->CSS['display_block'] = 'width:98%; margin:2px auto; padding:2px; line-height:1.3em; position:relative;'
    . ' background:#fff3f3; color:#660000; font-weight:normal; font-size:12px;'
    . ' border:4px solid #8b0000; text-align:left;';
  
  // debug
  if ( $this->debug ) $this->print_d('debugging is active for oid ' . $this->oid);
  if ( $this->debug ) $this->print_d('constructor complete for class ' . __CLASS__);
}
// END constructor

// destructor
function __destruct()
{
  if ( $this->debug ) $this->print_d("destroying class {$this->class_name} (oid: {$this->oid})");
}
// END destructor



/* ** PUBLIC METHODS ** */
// method: trigger
function trigger($message, $level=E_USER_NOTICE)
{
  trigger_error($message, $level);
}
// END method

// method: set error handler
function set_handler($Callback)
{
  $old_handler = '';      // return
  $this->handler_name = ( is_array($Callback) ) ? $Callback[1] : $Callback;
  $old_handler = set_error_handler($Callback);
  if ( $this->debug ) $this->print_d('setting error handler to ' . $this->handler_name);
  return $old_handler;
}
// END method


// method: set error handler according to release stage
function set_handler_by_release_stage($stage)
{
  if ( $this->debug ) $this->print_d("resetting handler by stage");
  $name = '';
  
  if ( $stage == CEO_ALPHA ) $name = 'alpha_handler';
  elseif ( $stage == CEO_BETA ) $name = 'beta_handler';
  elseif ( $stage == CEO_FINAL ) $name = 'production_handler';
  else 
  {
    $this->trigger("release stage [$stage] not recognized -- handler set to production", E_USER_WARNING);
    $name = 'production_handler';
  }
    
  $this->set_handler(array(&$this,$name));  
  return;
}
// END method


// method: kernel handler
function kernel_handler($num, $msg, $file, $line, $VARDUMP)
{
  /* NOTE
    The kernel should be as flawless as possible.  And the only person who should
    really ever have to see these is me, the developer.  We don't display trivial
    errors, but we do log them for display in comments.  More serious errors should
    cause a panic and trigger an email.
  */

  if ( $this->debug ) $this->print_d('error triggered -> handled by ' . __FUNCTION__);
  
  // collect data
  $this->_collect_error_data($num, $msg, $file, $line);
  
  // run filters
  // filter: trivial
  if ( $this->is_trivial )
  {
    if ( $this->debug ) $this->_display();
    $this->_log_strict();    
    return;
  }
  
  // filter: serious
  if ( $this->is_serious )
  {
    if ( $this->debug ) $this->_display();
    $this->_log();  
    $this->_email();
    
    // abort
    $strict_dump = print_r($this->_STRICT_LOG, 1);
    $notice = <<<HTML
    
<!-- STRICT LOG
count: {$this->strict_count}

$strict_dump

-->
    
HTML;
    $this->_abort($notice);
  }
  
  return;
}
// END method


// method: alpha handler
function alpha_handler($num, $msg, $file, $line)
{
  if ( $this->debug ) $this->print_d('error triggered -> handled by ' . __FUNCTION__);
  
  // collect data
  $this->_collect_error_data($num, $msg, $file, $line);
  
  // filters
  
    // trivial
    if ( $this->is_trivial )
    {
      $this->_log_strict();
      if ( $this->filter_trivial ) 
      {
        #if ( $this->debug ) $this->print_d("trival error: {$this->type} &raquo; <i>{$this->msg}</i> [l. {$this->trigger_line} in {$this->trigger_basename}] (#{$this->strict_count})");
        return;
      }
    }
  
  // reporting    
  $this->_display();
  $this->_display_in_comment();
  $this->_log();  
  if ( $this->email_alpha ) $this->_email();
  
  return;
}
// END method


// method: beta handler
function beta_handler($num, $msg, $file, $line)
{
  $this->print_d(__FUNCTION__ . ' in dev');
  if ( $this->debug ) $this->print_d('error triggered -> handled by ' . __FUNCTION__);
  $this->_collect_error_data($num, $msg, $file, $line);  
  $this->_display();
  $this->_log();  
  $this->_email();
  return;
}
// END method


// method: production handler
function production_handler($num, $msg, $file, $line)
{
  $this->print_d(__FUNCTION__ . ' in dev');
  if ( $this->debug ) $this->print_d('error triggered -> handled by ' . __FUNCTION__);
  $this->_collect_error_data($num, $msg, $file, $line);  
  $this->_display();
  $this->_log();  
  $this->_email();
  return;
}
// END method


// method: print_d
function print_d($message, $color='#c33')
{
  $out = "<div style='line-height:1.5em; font-family:monospace; color:$color;'>$message</div>";
  echo $out;
  return;
}
// END method

// method: print_r
function print_r($Mixed)
{
  $return = htmlspecialchars(print_r($Mixed, 1));
  $return = "<pre>$return</pre>";
  return $return;
}
// END method

// method: dump
function dump()
{
  echo $this->print_r($this);
  return;
}
// END method



/* ** PRIVATE METHODS ** */
// method: display error
function _display()
{
  if ( $this->debug ) $this->print_d("display error #{$this->count}");
  $output = $this->_build_display();
  print $output;
  return;
}
// END method


// method: display in comment
// for discreet output to browser, esp. useful in beta
function _display_in_comment()
{
  if ( $this->debug ) $this->print_d("display in comment error #{$this->count}");
  print "<!-- $comment -->";
  return;
}
// END method


// method: dev
function _log()
{
  $is_logged = 0;   // return
  static $cycle = 0;
  $output = '';

  if ( $this->debug ) $this->print_d("logging error to {$this->log_path}");
  
  // kill logging
  if ( $this->kill_logging )
  {
    if ( $this->debug ) $this->print_d('error logging is off');
    return;
  }

  // control log size
  if ( !file_exists($this->log_path) || ( $cycle == 0 && filesize($this->log_path) > $this->max_log_size ) )
  {
    if ( $this->debug ) $this->print_d("error log size > max [{$this->max_log_size}] -- will overwrite");
    fopen($this->log_path, 'w+');
    $output = 'CEO error file regenerated ' . date('r') . "\n";
  }

  // counter
  $_SESSION[$this->_sid]['log_called'] = ( isset($_SESSION[$this->_sid]['log_called']) ) ? $_SESSION[$this->_sid]['log_called'] + 1 : 1;
  $cycle++;

  // cycle header (first cycle call)
  if ( $cycle == 1 )
  {
    $_call_file = $_SERVER['SCRIPT_NAME'];
    $_datetime = date('Y-m-d h:i:s');
    $_sid = ( session_id() ) ? session_id() : 'N/A';
    $_g1 = ( !empty($_GET) ) ? 1 : 0;
    $_p1 = ( !empty($_POST) ) ? 1 : 0;
    $_c1 = ( !empty($_COOKIE) ) ? 1 : 0;
    $output .= <<<OUTPUT


*** NEW ERROR SCRIPT CYCLE [SESSION ID: {$_sid}] ***
SCRIPT NAME:{$_call_file}
GPC:{$_g1}{$_p1}{$_c1}
DATETIME: {$_datetime}
***********************************************

OUTPUT;
  }
  
  // error notice
  $_utime = microtime();
  $_count = sprintf("%03s", $_SESSION[$this->_sid]['log_called']);
  $_notice .= "$_count [$_utime] - {$this->trigger_basename}:{$this->trigger_line} :: [{$this->type}] {$this->msg} \n";
  $output .= $_notice;
  
  // log
  if ( !error_log($output, 3, $this->log_path) )
  {
    print "<!-- debug log failed: $_notice -->";
    return 0;
  }
  
  return 1;
}
// END method


// method: dev
function _email()
{
  $is_sent = 0;   // return
  global $PROJECT;
  
  // request release
  if ( !$this->_release_email() ) return 0;

  // get framework mailer object
  if ( !$this->_Mailer ) $this->_Mailer = CeoFramework::staticLoadMailer();
  $this->print_d('mailer object: ' . $this->print_r($this->_Mailer));
  
  // prepare email
  $to_email = $PROJECT['contact_email'];
  $to_name = 'Admin';
  $subject = "{$PROJECT['name']} Error: *{$this->type}* on {$this->date}";
  $dump_data = $this->print_r($this);
  $body = <<<TEXT
  
ERROR REPORT

ERROR SUMMARY
{$this->summary}

OBJECT DUMP
$dump_data

TEXT;

  // append error log
  $this->print_d('append error log to Err email');
 
  // send email
  if ( $is_sent = $this->_Mailer->send($to_email, $to_name, $subject, $body) )
  {
    if ( $this->debug ) $this->print_d("error email has been sent to project contact [$to_email]", 'purple');
    $this->_script_emails_sent++;
    $_SESSION[$this->_sid]['email_count']++;
    $this->_write_timelog();
  }
  else
  {
    if ( $this->debug ) $this->print_d('failed to send error email', 'magenta');
  }  
  
  return $is_sent;
}
// END method


// method: collect error data
function _collect_error_data($num, $msg, $file, $line)
{
  // build trace stack
  $this->trigger_file = '';
  $this->trigger_line = '';
  $this->bugtrace = $this->_get_bugtrace();
  $this->trigger_basename = basename($this->trigger_file);

  // basic data
  $this->num  = $num;
  $this->msg  = $msg;
  $this->file = $file;
  $this->line = $line;
  
  // contextual data
  $this->uts    = time();
  $this->date   = date("Y-m-d H:i:s (T)");
  $this->server = $_SERVER['SERVER_NAME'];
  
  // extended data
  $this->type = ( isset($this->_TYPE[$num]) ) ? $this->_TYPE[$num] : 'unrecognized';
  $this->basename = basename($file);
  $this->is_index_notice = ( $num == E_NOTICE && substr($msg, 0, 17) == 'Undefined index: ' ) ? 1 : 0;
  $this->is_strict = ( defined('E_STRICT') && $num == E_STRICT ) ? 1 : 0;
  $this->is_notice = ( $num == E_NOTICE || $num == E_USER_NOTICE ) ? 1 : 0;
  $this->is_trivial = ( $this->is_index_notice || $this->is_strict || $this->is_notice ) ? 1 : 0;
  $this->is_serious = $num & $this->_MASK['serious'];
  $this->is_fatal = $num & $this->_MASK['fatal'];
  
  // the counting
  $this->count++;
  if ( $this->is_index_notice ) $this->strict_count++;
  if ( $this->is_strict ) $this->strict_count++;
  
  // summary
  $this->summary = "error: {$this->type} &raquo; <i>{$this->msg}</i> [l. {$this->trigger_line} in {$this->trigger_basename}] (#{$this->count})";

  
  if ( $this->debug ) $this->print_d("collected data for error #{$this->count}");
  return;
}
// END method


// method: build html for display
function _build_display()
{
  $html = '';     // return
  
  $html = <<<HTML

<!-- ERROR #{$this->count} -->
<div style="{$this->CSS['display_block']}">
 <div style="{$this->CSS['display_count']}">{$this->count}</div>
 <div style="{$this->CSS['display_header']}"><b>{$this->type}</b> ($this->num) &raquo; <i>{$this->msg}</i></div>
 <div style="{$this->CSS['display_footer']}">l. {$this->trigger_line} in {$this->trigger_basename}</div>
 <div style="{$this->CSS['display_trace']}">{$this->bugtrace}</div>
</div>
  
HTML;
  
  return $html;
}
// END method


// method: get bugtrace
function _get_bugtrace()
{
  $bugtrace = '';    // return;
  $stack_dump = '';
  
  // side effects (reset triggers)
  $this->trigger_file = '';   
  $this->trigger_line = '';
  
  // get debug stack and stack depth
  $STACK = debug_backtrace();
  #$this->print_d($this->print_r($STACK));  // meta-debugging!
  
  /* now, to eliminate any internal error-handling steps from the stack and
    locate the trigger file and line.  A little harder than it seems at first
    glance.  For one, we want to preserve trigger_error step when called
    directly, but eliminate it when called by err object's trigger method.  So,
    my solution?
    
    Note: Remember, debug_backtrace counts backward (this function, last called, is
      element 0 in the array)
       
    1. Reverse STACK array
    2. Loop through it looking for internal error functions (will be in array
      _ERR_FUNCTION.)  As soon as we hit one, break.
    3. Last step should contain triggers.
      
    Initially I forgot about array_reverse and had a much more complicated method, 
    but this should work.  Let's see... excellent.  
  */ 
  
  // seed our stack-breaker
  $this->_ERR_FUNCTION[] = $this->handler_name;
  $this->_ERR_FUNCTION[] = __FUNCTION__;  # for consistency, as not really needed -- we'll never get this far.
  
  // reverse array
  $RSTACK = array_reverse($STACK);
  #$this->print_d($this->print_r($RSTACK));  // meta-debugging!
  
  // build stack
  $step_num = 0;
  foreach ( $RSTACK as $_STEP ) 
  {
    // get step data
    $_function = ( isset($_STEP['function']) ) ? $_STEP['function'] : '';
    $_file = str_replace('\\', '/', $_STEP['file']);
    $_line = $_STEP['line'];
    $_path = substr(dirname($_file), strrpos(dirname($_file), '/') + 1) . '/' . basename($_file);
    $_args = ( !empty($_STEP['args']) ) ? htmlspecialchars(implode(', ', $_STEP['args'])) : '';
    if ( strlen($_args) > 100 ) $_args = substr($_args, 0, 100) . '...';
    
    // look for a break
    if ( in_array($_function, $this->_ERR_FUNCTION) ) { break; }
    
    // build step output
    $step_num++;
    $_fx = ( !empty($_function) && $_function != 'unknown' ) ? "&raquo; fx $_function($_args)" : '';
    $stack_dump .= <<<HTML
    
<div class="ceo_stack_level">
Step #{$step_num} &raquo; 
{$_path}:{$_line} $_fx
</div>
    
HTML;

  }
  
  // triggers should be last line
  $this->trigger_file = $_file;   
  $this->trigger_line = $_line;
  
  // wrap bugtrace
  $bugtrace = <<<HTML

<div style="{$this->CSS['bugtrace']}">
$stack_dump
</div>

HTML;
 
  return $bugtrace;
}
// END method



// method: abort
function _abort($notice)
{
  $strict_dump = print_r($this->_STRICT_LOG, 1);

  $html = <<<HTML
  
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Framework Error</title>
</head>
<body>


<!-- fatal error -->
<div style="margin:10%; color:#993333; font-family:arial,sans-serif; text-align:center;">
<!-- {$this->num} : {$this->msg} ({$this->basename}:{$this->trigger_line}) -->
<h2>unable to load page</h2>
<p style='color:#666;'>site administrators have been notified</p>

<div class="abort_notice">
$notice
</div>

<p style="font-size:12px; text-align:right;"><a href="/">home page</a></p>
</div>

</body>
</html>

HTML;
  
  die($html);
}
// END method


// method: read timelog
function _log_strict()
{
  $this->_STRICT_LOG[] = "index notice ({$this->num}) at {$this->file}.{$this->line} : {$this->msg}";
  return;
}
// END method


// method: read timelog
function _release_email()
{
  $release = 0;   // return
  
  // check script limit
  if ( $this->_script_emails_sent > $this->script_email_limit ) 
  {
    if ( $this->debug ) $this->print_d("script err email limit ({$this->script_email_limit}) reached -- email will not be released");
    return 0;
  }
  
  // check session limit
  if ( $_SESSION[$this->_sid]['email_count'] > $this->session_email_limit )
  {
    if ( $this->debug ) $this->print_d("session err email limit ({$this->session_email_limit}) reached -- email will not be released");
    return 0;
  }
  
  // check inter-session interval
  if ( !$last_email_uts = $this->_read_timelog() ) 
  {
    if ( $this->debug ) $this->print_d('unable to read timelog -- email will not be released');
    return 0;
  }
  $interval = time() - $last_email_uts;
  
  if ( $interval < $this->session_email_delay )
  {
    if ( $this->debug ) $this->print_d("last err email sent $interval s ago, min delay is {$this->session_email_delay} s -- email will not be released");
    return 0;
  }
  
  // we can release email
  return 1;  
}
// END method


// method: read timelog
function _read_timelog()
{
  $uts = 0;    // return

  // open file
  if ( !$_handle = @fopen($this->_timelog_path, 'rb') )
  {
    if ( $this->debug ) $this->print_d("unable to open err timelog [{$this->_timelog_path}] for reading");
    return 0;
  }
  
  // read contents
  $_content = '';
  while ( !feof($_handle) ) 
  {
    $_buffer = fgets($_handle, 4096);
    $_content .= trim($_buffer);
  }
  
  // validity check
  $uts = trim($_content);
  $uts = ( !empty($uts) || is_numeric($uts) ) ? $uts : 0;
  if ( $this->debug ) $this->print_d("Err email timelog reading: $uts");
  
  @fclose($_handle);
  
  // return
  return $uts;
}
// END method

// method: write timelog
function _write_timelog()
{
  $uts = time();
  
  if ( !$_handle = @fopen($this->_timelog_path, 'w+') )
  {
    if ( $this->debug ) $this->print_d("unable to open err timelog [{$this->_timelog_path}] for writing");
    return;
  }
  
  if ( @fwrite($_handle, $uts) === FALSE )
  {
    if ( $this->debug ) $this->print_d('failed to write to timelog');
    return;
  }

  @fclose($_handle);
}
// END method

function _set_filename() { $this->_filename = basename(__FILE__); }
function _set_dirpath() { $this->_dirpath = dirname(__FILE__) . $this->DS; }

} // end class
/*____________________________________________________________________________*/

?>
